package com.example.web.config;

import com.alibaba.fastjson.JSON;
import com.example.common.utils.DateUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.StringJoiner;

/**
 * <p>
 *  以 " [TIME]:{}ms, [COUNT]:{} [SQL]:{}" 格式打印SQL
 * </p>
 *
 * @author 12071
 * @date 2022/8/15 15:15
 */
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
@Slf4j
@Component
public class SqlLogInterceptor implements Interceptor {
    private static final Logger LOGGER = LoggerFactory.getLogger("sql_log");

    private static final int PARAMTER_WARN_THRESHOLD = 1000;
    private static final Long SLOW_SQL_TIME_THREHOLD = 200L;

    private Properties properties;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        String sqlId = mappedStatement.getId();
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);

        if (invocation.getArgs().length == 6) {
            boundSql = (BoundSql) invocation.getArgs()[5];
        }

        Configuration configuration = mappedStatement.getConfiguration();

        long start = System.currentTimeMillis();

        Object returnValue = invocation.proceed();

        boolean returnValueEnable = checkReturnValueEnable();
        if (returnValueEnable) {
            LOGGER.info("returnValue:{}", JSON.toJSONString(returnValue));
        }

        int updateCnt = 0;
        if (returnValue instanceof List) {
            List resultSetList = (List) returnValue;
            if (resultSetList != null) {
                updateCnt = resultSetList.size();
            }
        } else if (returnValue instanceof Integer) {
            updateCnt = ((Integer) returnValue).intValue();
        } else {
            LOGGER.warn("returnValue:{}", returnValue);
        }

        long end = System.currentTimeMillis();
        long time = (end - start);
        if (time >= SLOW_SQL_TIME_THREHOLD) {
            LOGGER.warn(">{}, " + "\n" + " [TIME]:{}ms, [COUNT]:{} [SQL]:{}", sqlId, time, updateCnt, showSql(configuration, boundSql));
        } else {
            LOGGER.info(">{}, " + "\n" + " [TIME]:{}ms, [COUNT]:{} [SQL]:{}", sqlId, time, updateCnt, showSql(configuration, boundSql));
        }
        return returnValue;
    }

    private boolean checkReturnValueEnable() {
        if (properties == null) {
            return false;
        }
        String property = properties.getProperty("outputReturnValue", "false");
        return Boolean.parseBoolean(property);
    }

    private static String getParameterValue(Object obj) {
        StringJoiner value = new StringJoiner("'", "'", "'");

        if (obj == null) {
            return "";
        }

        if (obj instanceof Date) {
            value.add(DateUtils.toString(((Date) obj).getTime(), "yyyy-MM-dd HH:mm:ss"));
        } else if (obj instanceof LocalDate) {
            value.add(DateUtils.toString((LocalDate) obj, "yyyy-MM-dd"));
        } else if (obj instanceof LocalDateTime) {
            value.add(DateUtils.toString((LocalDateTime) obj, "yyyy-MM-dd HH:mm:ss"));
        } /*else if (obj instanceof TemporalAccessor) {
            value.add(DATE_TIME_FORMATTER.format((TemporalAccessor) obj));
        } */ else {
            value.add(obj.toString());
        }
        return value.toString();
    }

    public static String showSql(Configuration configuration, BoundSql boundSql) {

        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");

        int paramMappingSize = parameterMappings.size();
        if (paramMappingSize > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);

                if (paramMappingSize > PARAMTER_WARN_THRESHOLD) {
                    LOGGER.warn("parameters is too much, now:{}, greater than:{}, please check your sql", paramMappingSize, PARAMTER_WARN_THRESHOLD);
                }

                long start = System.nanoTime();
                String[] sqlSplits = sql.split("\\?");
                int sqlSplitLen = sqlSplits.length;

                if (sqlSplitLen != paramMappingSize + 1 && sqlSplitLen != paramMappingSize) {
                    LOGGER.warn("sqlSplitLen and parameterMapping maybe wrong{}, {}", sqlSplitLen, paramMappingSize);
                }

                int i = 0;
                StringBuilder sb = new StringBuilder();
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    sb.append(i < sqlSplitLen ? sqlSplits[i] : "_");
                    if (metaObject.hasGetter(propertyName)) {
                        sb.append(getParameterValue(metaObject.getValue(propertyName)));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        sb.append(getParameterValue(boundSql.getAdditionalParameter(propertyName)));
                    }
                    i++;
                }

                for (int j = i; j < sqlSplitLen; j++) {
                    String placeholder = sqlSplits[j];
                    sb.append(placeholder != null ? placeholder : "");
                }
                sql = sb.toString();

                LOGGER.debug("replace times:{}, cost:{}ns", i, System.nanoTime() - start);
            }
        }
        return sql;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}

